# -*- coding: utf-8 -*-
"""Defines base classes for physical objects.

StaticSprite -- an immobile object that is represented by an image

StaticEdge -- an immobile object that is represented by a line segment

Dynamic -- an object that can be affected by other objects in the world
           and that is represented by an image
"""

import math

import Box2D.b2 as b2
import pyglet

from camera import world_to_pixels, pixels_to_world
import config
from constants import APPLY_GRAVITY
from constants import MAX_ANGLE_AGAINST_GROUND_NORMAL_FOR_JUMP as MAX_ANGLE
from constants import STRENGTH_OF_DAMP as DAMP
from constants import STRENGTH_OF_GRAVITY as G
from debug import msg


class Entity(object):
    """Base class for physical objects.

    Public methods:
    update(dt) -- Updates the state of the object given that dt seconds have passed.
    """

    def __init__(self, mode):
        """mode is the mode that this entity is created in. """
        self.mode = mode

    def update(self, dt):
        """Update the state given that dt seconds have passed
        since the last update."""
        pass


class StaticSprite(Entity):
    def __init__(self, mode, img, anchor=(0.5, 0.5), x=0, y=0,
                 fixture_box=None, rot=0, e_type=None):

        super(StaticSprite, self).__init__(mode)

        self.sprite = pyglet.sprite.Sprite(img, x=world_to_pixels(x),
                                           y=world_to_pixels(y),
                                           batch=self.mode.batch_static)
        self.sprite.rotation = - rot * 180 / math.pi

        self.body = mode.world.CreateStaticBody(position=(x, y), angle=rot)

        # Converts image proportions to world-coordinates and puts
        # them in a tuple for ease of use.
        try:
            # Tries to handle img as an animation.
            image_proportions = (pixels_to_world(img.get_max_width()),
                                 pixels_to_world(img.get_max_height()))
        except AttributeError:
            # Tries to handle img as an image.
            image_proportions = (pixels_to_world(img.width),
                                 pixels_to_world(img.height))

        if fixture_box is not None:
            self.body.CreatePolygonFixture(box=fixture_box,
                                           userData={"type": e_type,
                                                     "self": self},
                                           friction=0.1)
        else:
            # Adjust center of fixture to align with image.
            center_shift = b2.vec2(0, 0)
            for i in range(2):
                center_shift[i] = (0.5 - anchor[i]) * image_proportions[i]

            self.body.CreatePolygonFixture(box=(image_proportions[0] / 2.0,
                                                image_proportions[1] / 2.0,
                                                center_shift, 0),
                                           userData={"type": e_type,
                                                     "self": self},
                                           friction=0.2)

        self.fixture = self.body.fixtures[0]


class StaticEdge(Entity):
    def __init__(self, mode, vertices, e_type=None, isSensor=False):
        super(StaticEdge, self).__init__(mode)
        self.body = mode.world.CreateStaticBody(position=vertices[0])

        new_vertices = [(0,0)]
        for p in vertices[1:]:
            nx = p[0] - vertices[0][0]
            ny = p[1] - vertices[0][1]
            new_vertices.append((nx, ny))

        self.body.CreateEdgeFixture(vertices=new_vertices,
                                    isSensor=isSensor,
                                    userData={"type": e_type,
                                              "self": self})

class Dynamic(Entity):
    fixed_rotation = False
    anchor = (0.5, 0.5)
    e_type = "dynamic"
    friction = 0.3
    restitution = 0.0
    density = 1

    def __init__(self, mode, img, start_configuration,
                 front_is_left=False,
                 portalable=None):

        super(Dynamic, self).__init__(mode)

        ((x, y), (vel_x, vel_y), rot) = start_configuration

        self.portalable = portalable

        self.previous_pos = None
        self.just_rotated = False

        self.front_is_left = front_is_left

        # If an is partially through a portal, these records which portal
        # it entered and exits through
        self.entrance_portal = None
        self.exit_portals = []

        self.ps = []         # Testing

        self.gravity_dir = b2.vec2(math.sin(rot), - math.cos(rot))

        if front_is_left:
            self.face_dir = b2.mat22(-math.pi/2) * self.gravity_dir
        else:
            self.face_dir = b2.mat22(math.pi/2) * self.gravity_dir

        self.body = mode.world.CreateDynamicBody(
            position=(x, y), angle=rot,
            linearVelocity=(vel_x, vel_y),
            fixedRotation=self.fixed_rotation,
            linearDamping=DAMP)

        # Converts image proportions to world-coordinates and puts
        # them in a tuple for ease of use.
        image_proportions = (pixels_to_world(img.width),
                             pixels_to_world(img.height))

        # Adjust center of fixture to align with image.
        center_shift = b2.vec2(0, 0)
        for i in range(2):
            center_shift[i] = (0.5 - self.anchor[i]) * image_proportions[i]

        # Used for box to center pendulum
        self.init_world_center = - center_shift + (x, y)

        self.fixture = self.body.CreatePolygonFixture(box=(image_proportions[0] / 2.0,
                                                           image_proportions[1] / 2.0,
                                                           center_shift, 0),
                                                      friction=self.friction,
                                                      density=self.density,
                                                      userData={"type" : self.e_type, "self": self},
                                                      restitution=self.restitution,
                                                      )

        # Create a sprite coupled to the physical body
        self.sprite = pyglet.sprite.Sprite(
            img, batch=self.mode.batch_dynamic)
        self.update_sprite()

    def update_sprite(self):
        """Move sprite to the same location and
        orientation as the physicsobject"""

        self.sprite.x = world_to_pixels(self.body.position[0])
        self.sprite.y = world_to_pixels(self.body.position[1])

        # Pyglet sprites rotate clockwise
        self.sprite.rotation = (-self.body.angle) * (180 / math.pi)

    def update(self, dt):
        # Apply gravity
        if APPLY_GRAVITY:
            self.body.ApplyForce(G * self.body.mass * self.gravity_dir,
                                 self.body.worldCenter)

        self.update_sprite()

        if config.debug:
            self.mode.renderer.batch = pyglet.graphics.Batch()
            self.mode.world.DrawDebugData()

    def is_grounded(self):
        """
        Checks if the entity is standing on ground, and returns a
        tuple containing a boolean and a list of tuples with objects
        considered ground and the contact points between that object
        and this.
        """
        def ground_check(player, normal):
            """
            Checks if the contact is with a ground-surface (i.e. at a
            certain angle).
            """
            g = player.userData["self"].gravity_dir.copy()
            g.Normalize()

            normal.Normalize()

            for i in range(2):
                if abs(g[i]) < 1e-3:
                    g[i] = 0

            angle = math.acos(g.dot(normal))

            return abs(angle - math.pi) < MAX_ANGLE

        retval = []
        grounded = False

        for c in self.body.contacts:
            if c.contact.touching:
                # Necessary workaround. contact.worldManifold.normal
                # has a faulty get-function, returns vector close to
                # zero.
                m = c.contact.worldManifold
                if c.contact.fixtureA.userData["self"] is self:
                    f = [c.contact.fixtureA, c.contact.fixtureB]
                    n = m.normal * -1
                else:
                    f = [c.contact.fixtureB, c.contact.fixtureA]
                    n = m.normal
                if ground_check(f[0], n) and not f[1].sensor:
                    grounded = True
                    if isinstance(f[1].userData["self"], Dynamic):
                        retval.append((f[1].userData["self"], m.points))

        return (grounded, retval)
